package com.wedimob.slideshare.task.io.network;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;

import com.google.gson.Gson;

public class JavaYoutubeDownloader extends Observable {

	public static String newline = System.getProperty("line.separator");
	private static final Logger log = Logger
			.getLogger(JavaYoutubeDownloader.class.getCanonicalName());
	private static final Level defaultLogLevelSelf = Level.FINER;
	private static final Level defaultLogLevel = Level.WARNING;
	private static final Logger rootlog = Logger.getLogger("");
	private static final String scheme = "http";
	private static final String host = "www.youtube.com";
	private static final char[] ILLEGAL_FILENAME_CHARACTERS = { '/', '\n',
			'\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"',
			':' };
	private static final String URL_REGEX = "url=(.*?)(&|;|,|$)";
	private static final String TAG_REGEX = "=(.+?)(,|;|:|&)";

	private static void usage(String error) {
		if (error != null) {
			System.err.println("Error: " + error);
		}
		System.err
				.println("usage: YouTubeDownloader VIDEO_ID DESTINATION_DIRECTORY");
		System.exit(-1);
	}

	private static class Progress implements Observer {
		@Override
		public void update(Observable o, Object arg) {
			if (o instanceof JavaYoutubeDownloader) {
				System.out.println("Progress " + arg + " %");
			}
		}

	}

	public static void main(String[] args) {
		if (args == null || args.length == 0) {
			usage("Missing video id. Extract from http://www.youtube.com/watch?v=VIDEO_ID");
		}
		try {
			setupLogging();

			log.fine("Starting");
			String videoId = null;
			String outdir = ".";
			if (args.length == 1) {
				videoId = args[0];
			} else if (args.length == 2) {
				videoId = args[0];
				outdir = args[1];
			}

			JavaYoutubeDownloader downloader = new JavaYoutubeDownloader(
					videoId, outdir);
			downloader.addObserver(new Progress());
			downloader.download(downloader.getUrls().urls.keySet().iterator().next());

		} catch (Throwable t) {
			t.printStackTrace();
		}
		log.fine("Finished");
	}

	private String videoId;
	private String outputDir;
	private VideoInfo info;
	private Integer progress;

	public JavaYoutubeDownloader(String videoId, String outputDir) {
		super();
		this.videoId = videoId;
		this.outputDir = outputDir;
	}

	private static String getExtension(String format) {
		if (format.contains("mp4"))
			return "mp4";

		if (format.contains("flv"))
			return "flv";

		if (format.contains("webm"))
			return "webm";

		if (format.contains("3gpp"))
			return "3gpp";

		return "mp3";
	}

	private static class JSONArgsResponse {
		public String url_encoded_fmt_stream_map;
		public String title;
	}

	private static class JSONResponse {
		public JSONArgsResponse args;
	}

	public static class VideoInfo {
		public String videoId;
		public String title;
		public Map<String, String> urls;
		public Map<String, Integer> sizes;
		public HttpClient httpclient;
		public HttpContext localContext;
		public String userAgent;
		public String encoding;
	}
	
	public static String humanReadableByteCount(long bytes, boolean si) {
	    int unit = si ? 1000 : 1024;
	    if (bytes < unit) return bytes + " B";
	    int exp = (int) (Math.log(bytes) / Math.log(unit));
	    String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
	    return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
	}

	public VideoInfo getUrls() throws UnsupportedEncodingException,
			IOException, URISyntaxException {
		if (info != null)
			return info;

		info = new VideoInfo();
		CookieStore cookieStore = new BasicCookieStore();
		info.encoding = "UTF-8";
		info.userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13";
		info.videoId = videoId;
		info.localContext = new BasicHttpContext();
		info.localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
		info.httpclient = new DefaultHttpClient();
		info.urls = new HashMap<String, String>();

		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("v", videoId));
		URI uri = getUri("watch", qparams);
		HttpGet httpget = new HttpGet(uri);
		httpget.setHeader("User-Agent", info.userAgent);
		HttpResponse response = info.httpclient.execute(httpget,
				info.localContext);
		HttpEntity entity = response.getEntity();
		if (entity != null && response.getStatusLine().getStatusCode() == 200) {
			InputStream instream = entity.getContent();
			String videoInfo = getStringFromInputStream(info.encoding, instream);
			if (videoInfo != null && videoInfo.length() > 0) {
				int beginIndex = videoInfo.indexOf("yt.playerConfig = ");
				int endIndex = videoInfo.indexOf("};", beginIndex + 18);
				String json = videoInfo
						.substring(beginIndex + 18, endIndex + 1);
				Gson gson = new Gson();
				JSONResponse jsonObj = gson.fromJson(json, JSONResponse.class);

				info.title = jsonObj.args.title;

				String urlMap = URLDecoder.decode(
						jsonObj.args.url_encoded_fmt_stream_map, "UTF-8");
				Pattern pattern = Pattern.compile(URL_REGEX,
						Pattern.CASE_INSENSITIVE);

				Pattern typePattern = Pattern.compile("type" + TAG_REGEX,
						Pattern.CASE_INSENSITIVE);
				Pattern qualityPattern = Pattern.compile("quality" + TAG_REGEX,
						Pattern.CASE_INSENSITIVE);
				Pattern sigPattern = Pattern.compile("sig" + TAG_REGEX,
						Pattern.CASE_INSENSITIVE);

				Matcher matcher = pattern
						.matcher(jsonObj.args.url_encoded_fmt_stream_map);
				Matcher typematcher = typePattern.matcher(urlMap);
				Matcher qualitymatcher = qualityPattern.matcher(urlMap);
				Matcher sigmatcher = sigPattern.matcher(urlMap);
				// calculate the number of matches
				int numOfMatches = 0;
				while (matcher.find()) {
					numOfMatches++;
				}
				matcher.reset();

				if (numOfMatches == 0)
					return null;

				String[][] links = new String[4][numOfMatches];
				int i = 0;
				while (matcher.find() && typematcher.find()
						&& qualitymatcher.find() && sigmatcher.find()) {

					links[0][i] = matcher.group(1);
					links[1][i] = typematcher.group(1);
					links[2][i] = qualitymatcher.group(1);
					links[3][i] = sigmatcher.group(1);

					i++;
					info.urls.put(links[1][i - 1] + ":" + links[2][i - 1],
							URLDecoder.decode(links[0][i - 1], "UTF-8")
									+ "&newshard=yes&signature="
									+ links[3][i - 1]);
				}
			}
		}

		info.sizes = new HashMap<String, Integer>();
		for (String quality : info.urls.keySet()) {
			String url = info.urls.get(quality);
			info.sizes.put(quality, getFileSize(url));
		}
		
		for (String quality : info.sizes.keySet()) {
			System.out.println(quality + " => " + humanReadableByteCount(info.sizes.get(quality), true));
		}

		return info;
	}

	private Integer getFileSize(String urli) {
		try {
			URL url = new URL(urli);
			URLConnection connection = url.openConnection();
			connection.connect();
			return connection.getContentLength();
		} catch (Exception e) {
			return 0;
		}
	}

	public Integer getProgress() {
		return progress;
	}

	public void setProgress(Integer progress) {
		if (progress != this.progress) {
			setChanged();
			notifyObservers(progress);
		}

		this.progress = progress;
	}

	public void download(String format) throws Throwable {
		if (info == null)
			getUrls();

		String downloadUrl = info.urls.get(format);
		String filename = cleanFilename(info.title);
		if (filename.length() == 0) {
			filename = info.videoId;
		}
		filename += "." + getExtension(format);
		File outputfile = new File(outputDir, filename);

		log.finer("Saving to " + outputfile);
		log.finer("From " + downloadUrl);

		if (downloadUrl != null) {
			downloadWithHttpClient(info, downloadUrl, outputfile);
		} else {
			throw new RuntimeException("Available format " + info.urls.keySet());
		}
	}

	private void downloadWithHttpClient(VideoInfo info, String downloadUrl,
			File outputfile) throws Throwable {
		HttpGet httpget2 = new HttpGet(downloadUrl);
		httpget2.setHeader("User-Agent", info.userAgent);
		HttpResponse response2 = info.httpclient.execute(httpget2,
				info.localContext);
		HttpEntity entity2 = response2.getEntity();
		if (entity2 != null && response2.getStatusLine().getStatusCode() == 200) {
			long length = entity2.getContentLength();
			InputStream instream2 = entity2.getContent();
			log.finer("Writing " + length + " bytes to " + outputfile);
			if (outputfile.exists()) {
				return;
			}
			FileOutputStream outstream = new FileOutputStream(outputfile);
			try {
				byte[] buffer = new byte[2048];
				int count = -1;
				long downloadBytes = 0;
				while ((count = instream2.read(buffer)) != -1) {
					outstream.write(buffer, 0, count);
					downloadBytes += count;
					setProgress((int) Math.round(100.0 * downloadBytes / length));
				}
				outstream.flush();
			} finally {
				outstream.close();
			}
		} else {
			throw new RuntimeException("HtmlError:"
					+ response2.getStatusLine().getStatusCode() + " for "
					+ downloadUrl);
		}
	}

	private String cleanFilename(String filename) {
		for (char c : ILLEGAL_FILENAME_CHARACTERS) {
			filename = filename.replace(c, '_');
		}
		return filename;
	}

	private URI getUri(String path, List<NameValuePair> qparams)
			throws URISyntaxException {
		URI uri = URIUtils.createURI(scheme, host, -1, "/" + path,
				URLEncodedUtils.format(qparams, "UTF-8"), null);
		return uri;
	}

	public static void setupLogging() {
		changeFormatter(new Formatter() {
			@Override
			public String format(LogRecord arg0) {
				return arg0.getMessage() + newline;
			}
		});
		explicitlySetAllLogging(Level.FINER);
	}

	private static void changeFormatter(Formatter formatter) {
		Handler[] handlers = rootlog.getHandlers();
		for (Handler handler : handlers) {
			handler.setFormatter(formatter);
		}
	}

	private static void explicitlySetAllLogging(Level level) {
		rootlog.setLevel(Level.ALL);
		for (Handler handler : rootlog.getHandlers()) {
			handler.setLevel(defaultLogLevelSelf);
		}
		log.setLevel(level);
		rootlog.setLevel(defaultLogLevel);
	}

	private String getStringFromInputStream(String encoding,
			InputStream instream) throws UnsupportedEncodingException,
			IOException {
		Writer writer = new StringWriter();

		char[] buffer = new char[1024];
		try {
			Reader reader = new BufferedReader(new InputStreamReader(instream,
					encoding));
			int n;
			while ((n = reader.read(buffer)) != -1) {
				writer.write(buffer, 0, n);
			}
		} finally {
			instream.close();
		}
		String result = writer.toString();
		return result;
	}
}